Skip to content

rpc, execution, db: FcuBackgroundCommit groundwork, coherent cache fixes - #21293

Closed
yperbasis wants to merge 51 commits into
mainfrom
alex/fcu_bg_commit_35
Closed

rpc, execution, db: FcuBackgroundCommit groundwork, coherent cache fixes#21293
yperbasis wants to merge 51 commits into
mainfrom
alex/fcu_bg_commit_35

Conversation

@yperbasis

@yperbasis yperbasis commented May 19, 2026

Copy link
Copy Markdown
Member

Summary

Groundwork for enabling FcuBackgroundCommit — the default flip and the standalone cache-retention increase from 0MB to 128MB are in the stacked #22269; defaults are unchanged here. With the flag on, the FCU response returns to the consensus client before the MDBX flush+commit lands: the commit runs in a background goroutine, and the ExecModule semaphore serializes successive FCUs, so FCU N+1 always reads FCU N's committed state. This PR makes RPC reads, the version-keyed state cache, and post-FCU consumers coherent under that mode. Several of the fixes matter already today, because state-change notifications are dispatched pre-commit.

RPC: head-sensitive reads resolve on a well-defined view

They see the pre-commit head everywhere or nowhere — never a mix.

Overlay view — the head and every dependent block-table read are served from the published BlockOverlay: bor getSnapshot/getAuthor/getSigners/getSnapshotProposer{,Sequence}/latest-block, eth_getBlockTransactionCountBy{Number,Hash}, graphql latest-block, debug_setHead, debug_getRawHeader, eth_getTransactionByHash, txpool_content{,From}, and erigon_getBlockByTimestamp. Behavior change on Polygon: for bor getSnapshot/getSigners/getSnapshotProposer{,Sequence}, nil/latest now resolves to the overlay-aware forkchoice/executed head instead of the header-stage tip (ReadCurrentHeader), so a catching-up node answers for its executed position rather than the downloaded-header tip — consistent with eth_blockNumber; getAuthor additionally fixes explicit-tag resolution (negative tags were cast to uint64 and returned "unknown block"). BaseAPI.headerByHash is overlay-aware, covering every by-hash consumer that stays on block tables. Each request pins one overlay read view up front and reuses it for all dependent reads, so an overlay unpublished mid-request cannot drop the request onto the older MDBX snapshot.

Committed view — the dependent reads use SD-temporal data, which the overlay does not expose, so tags resolve with nil filters and the bounds agree with the data: eth_getLogs/overlay_* range resolution (including eth_getLogs block-hash filters), trace_filter, eth_getProof, eth_simulateV1, debug_traceBlockBy*, debug_storageRangeAt, debug_accountRange, debug_accountAt (by-hash included — an overlay-resolved head would have no committed history), eth_getWitness. The filters-param contract is documented on rpchelper.GetBlockNumber. eth_getProof additionally keeps header lookup, commitment reconstruction, and state reads on the caller's single RO snapshot; shared branch-cache reads are bound-gated (servableUnderBound, #22467), so a concurrent commit cannot mix snapshots. Nil guards cover parity_listStorageKeys, trace_filter, and the eth_getProof header lookup.

Compatibility: on payload-building nodes, "pending" resolves to the latest executed block in every committed-view method that accepts the tag (eth_getLogs/overlay_*, trace_filter, debug_traceBlockByNumber, eth_simulateV1, eth_getProof, eth_getWitness). This keeps tag resolution aligned with the committed data those methods read. debug_accountRange keeps its explicit pending rejection.

Known embedded-daemon limitation: generic latest-state calls (eth_call, eth_getBalance, eth_getStorageAt, eth_getCode) resolve the overlay head while their temporal state reads stay on the committed snapshot — head N with state N-1 for the commit duration. The SD-aware temporal view needed to close this is tracked in #21314.

Coherent state cache: roots keyed by the version readers observe

db/kv/kvcache, execution/execmodule/notification_dispatcher.go. The pre-commit dispatch announces the post-commit PlainStateVersion, so version-keyed roots match exactly the readers that should see them: post-commit transactions hit the new canonical root, pre-commit transactions keep the previous one. Batch-fed storage and code entries use the key shapes readers look up (address+location; code by address). A view that outlives KeepViews version advances falls back to its own tx snapshot instead of erroring too old ViewID; reads through non-latest views are not cached (they bypass eviction accounting and would grow without bound). Retained roots share the configured memory budgets: state and code entries in older roots are cleared on version advance, and views pinned to them fall back to their own snapshots. Canonical roots start from their own batch, with no carry-over: the state-change producers don't announce every mutation, so a carried entry could stay stale — at a deliberate hit-rate cost: every version starts cold except the batch's changed keys, so hot-but-unchanged keys miss once per key per block, and steady-state hit rates are much lower than with carry-over. Completing the producers and restoring carry-over (which recovers the warmth) is tracked in #22276. The standalone daemon uses this coherent cache at every configured budget, in remote and --datadir modes alike. Positive budgets retain entries; 0MB retains none and disables new-block waiting, so reads fall back to the caller's tx snapshot while remaining coherent. The default remains 0MB until #22269 raises it to 128MB.

Engine: busy-tolerant batch execution

A background FCU commit briefly holds the exec semaphore, so ValidateChain/UpdateForkChoice can routinely return Busy. execDownloadedBatch waits it out via retryBusy: a ctx-aware 50 ms poll with a periodic debug log so a stuck commit surfaces.

Overlay safe-close invariant

db/kv/membatchwithdb. Shared-tx read views are safe only on the pure-Go memStore backing, whose Rollback/Close are no-ops on the data — that is what lets the bg-commit goroutine close the published overlay while RPC readers still iterate views. The memTx field's type restricts overlays to that backing.

Post-FCU readers wait for the commit

State-change events are dispatched pre-commit, so the notification stream alone does not guarantee MDBX contains the head. import_cmd and the execmodule tester call ExecModule.WaitIdle before opening a fresh tx after UpdateForkChoice.

Tests

TestStateChangeVersionMatchesCommitted pins announce-vs-committed version parity across {fg, bg} × {1-by-1, batched} (the batched case crosses the initial-cycle threshold, covering mid-FCU version bumps). kvcache tests pin fresh roots, retained-root aggregate budgets, reader-shaped feed keys, the evicted-view fallback, and non-latest no-cache. TestZeroBudgetRemoteCachePinsCommittedState pins snapshot-consistent reads with entry retention disabled. Overlay tests pin view selection in TestGetBlockTransactionCountByHash_SeesOverlayHead, TestDebugAccountAt_OverlayHeadHash_CommittedView, and TestGetLogsBlockHashUsesCommittedView, plus view lifetime under concurrent unpublish (the three *_PinsOverlayView tests). TestGetProofPinsReadSnapshot pins all proof reads to one RO snapshot; TestGetProofMissingHeader pins a clean error for a missing header. TestNotificationDispatchBackgroundCommit covers notification dispatch under background commit; multi-block bg-commit coverage lives in TestReorgBackAndForwardIntoCanonicalChain (bg mode) and TestInsertBlocksWithBatchedFCU_BadBlockRecovery_Background.

Safety (with the flag enabled)

FCU sequencing. The bg goroutine releases the exec semaphore only after the commit completes and PublishOverlay(nil). AssembleBlock/ValidateChain share the semaphore, so engine_newPayload/engine_getPayload serialize behind a pending commit — ~commit-duration added latency; a SYNCING response in a pipelined burst is spec-valid and self-heals.

Embedded rpcdaemon. Overlay-aware paths read FCU N's block-table writes pre-commit. In-flight readers keep their pinned view across unpublish; new readers cascade to MDBX, which by then contains N. No stale window for block-table paths; latest-state calls carry the #21314 limitation above.

Standalone rpcdaemon (remote or with datadir). With a positive entry-retention budget, cache entries are keyed by PlainStateVersion, bumped inside the commit batch and announced at the post-commit value — a daemon tx opened during the commit window resolves the N-1 root, so pre-commit batch data is never served against the older head. At 0MB, no entries are retained and each view falls back to its own tx snapshot, preserving the same consistency without serving batch data. Incomplete producer batches can still leave a retained entry stale for one version (#22276).

Interplay with #21414 (FCU semaphore decouple)

#21414 releases the exec semaphore as soon as updateForkChoice returns and moves the commit to a FIFO background worker, chaining FCU N+1's in-memory state onto FCU N's in-flight commit generation. Whichever PR lands second must re-validate the premises stated above in semaphore terms:

  • "FCU N+1 reads FCU N's committed state" becomes "reads the chained in-flight generation" — TestStateChangeVersionMatchesCommitted and the cache's pre-commit-window reasoning are the regression net.
  • WaitIdle currently implies the commit has landed; import_cmd and the execmodule tester rely on that.
  • Overlay fallbacks rely on PublishOverlay(nil) happening only after the commit lands; the worker must preserve that ordering.
  • The Busy window retryBusy waits out mostly disappears (the helper stays correct, just rarely loops).

Flag gating composes: #21414 inherits --fcu.background.commit (default false); the flip stays with #22269.

Routes head-sensitive RPC reads (ReadCurrentHeader, ReadHeadHeaderHash,
GetLatestBlockNumber) through the SharedDomains overlay via existing
Filters.WithOverlay/WithTemporalOverlay, so the FCU response can return
before the MDBX commit lands without RPC consumers seeing stale chain
heads. Coherent cache for remote rpcdaemon already receives pre-commit
StateChanges and serves the matching state-version root.

See #21008 for the motivating regression (~+80ms p50
FCU latency + multi-second burst tails on main vs release/3.4).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Enables FcuBackgroundCommit by default and updates JSON-RPC handlers to read head-sensitive data through the SharedDomains overlay so RPC responses can reflect the new FCU head before the MDBX commit/fsync lands.

Changes:

  • Flip ethconfig.Defaults.FcuBackgroundCommit to true and update the rationale comment to reflect current overlay + notification/coherent-cache wiring.
  • Wrap a set of head-sensitive rawdb.Read* / rpchelper.Get{Latest,Safe,Finalized}BlockNumber call sites with filters.WithOverlay / WithTemporalOverlay.
  • Minor call-site refactors (introducing overlayTx locals) to reuse overlay-backed transactions.

Reviewed changes

Copilot reviewed 17 out of 17 changed files in this pull request and generated no comments.

Show a summary per file
File Description
rpc/jsonrpc/txpool_api.go Read current header via overlay for txpool content endpoints.
rpc/jsonrpc/trace_filtering.go Use overlay-backed head hash/number when ToBlock is omitted.
rpc/jsonrpc/parity_api.go Read current block number via overlay for parity storage-key listing.
rpc/jsonrpc/overlay_api.go Use overlay-backed latest block number when capping ranges.
rpc/jsonrpc/graphql_api.go Resolve “latest” block number via overlay for GraphQL API.
rpc/jsonrpc/eth_txs.go Read current header via overlay for pending-tx responses.
rpc/jsonrpc/eth_system.go Use temporal overlay for gas oracle backend and overlay for head header reads (base fee/blob base fee).
rpc/jsonrpc/eth_simulation.go Compare requested block to overlay-backed latest head to avoid “future block” false positives.
rpc/jsonrpc/eth_receipts.go Use overlay-backed latest block number in max-uint32 capping path.
rpc/jsonrpc/eth_call.go Use overlay-backed latest block number when building proof/witness preconditions.
rpc/jsonrpc/eth_block.go Use overlay-backed latest block number for transaction-count-by-number guard.
rpc/jsonrpc/erigon_receipts.go Use overlay-backed latest block number for log range defaults.
rpc/jsonrpc/erigon_block.go Read current header via overlay for timestamp-based block lookup.
rpc/jsonrpc/debug_execution_witness.go Use overlay-backed latest block number when building expected post-state.
rpc/jsonrpc/debug_api.go Use overlay-backed latest block number for debug_setHead baseline.
rpc/jsonrpc/bor_api_impl.go Use overlay-backed latest header/number in several Bor RPCs.
node/ethconfig/config.go Default FcuBackgroundCommit to true and expand explanatory comment.
Comments suppressed due to low confidence (4)

rpc/jsonrpc/parity_api.go:80

  • bn is read through the overlay, but subsequent reads (_txNumReader.Min and tx.RangeAsOf) still use the original temporal tx. During a background-commit window this can leave parity_listStorageKeys querying state/txnums from the committed DB while targeting the overlay head number, producing stale or inconsistent results. Consider wrapping the temporal tx once (e.g., via filters.WithTemporalOverlay) and using that wrapped tx consistently for the latest-state reader, txnum lookup, and the RangeAsOf scan.
	bn := rawdb.ReadCurrentBlockNumber(api.filters.WithOverlay(tx))
	minTxNum, err := api._txNumReader.Min(ctx, tx, *bn)
	if err != nil {
		return nil, err
	}

rpc/jsonrpc/trace_filtering.go:351

  • toBlock is now derived from an overlay-backed head hash/number, but the rest of the method still passes the original dbtx into filterV3, which computes txnum bounds and scans indexes against the committed DB view. This means the requested range can reference an overlay head while the underlying trace scan is still anchored to the pre-commit state (and the block-range limit check uses the overlay height). Consider using a temporal overlay tx (filters.WithTemporalOverlay(dbtx)) and passing that through to filterV3 (and any txnum/index reads) so the head number and the scanned data come from the same view.
	if req.ToBlock == nil {
		overlayTx := api.filters.WithOverlay(dbtx)
		headNumber, err := api._blockReader.HeaderNumber(ctx, overlayTx, rawdb.ReadHeadHeaderHash(overlayTx))
		if err != nil {
			return err
		}
		toBlock = *headNumber

rpc/jsonrpc/bor_api_impl.go:111

  • latestBlockNum is resolved using an overlay-backed tx, but the subsequent HeaderByNumber call still uses the original tx. During background commit this can cause the latest header lookup to miss the overlay head (returning errUnknownBlock even though the overlay has the header). Consider reusing the same overlay-wrapped tx for the HeaderByNumber/HeaderByHash reads in the “latest” path.
	//nolint:nestif
	if blockNrOrHash == nil {
		latestBlockNum, err2 := rpchelper.GetLatestBlockNumber(api.filters.WithOverlay(tx))
		if err2 != nil {
			return accounts.NilAddress, err2
		}
		header, err = api._blockReader.HeaderByNumber(ctx, tx, latestBlockNum)
	} else {

rpc/jsonrpc/eth_block.go:383

  • This method now uses an overlay-backed tx to compute latestBlockNumber, but it still reads the block body/tx count through the original tx later in the function. In a background-commit window, blockNum can be the overlay head while _blockReader.Body on the committed tx returns nil, so the RPC may still return null for latest. Consider using the same overlay-wrapped view for the subsequent body read when serving head-sensitive queries.
	latestBlockNumber, err := rpchelper.GetLatestBlockNumber(api.filters.WithOverlay(tx))
	if err != nil {
		return nil, err
	}
	if blockNum > latestBlockNumber {

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

yperbasis and others added 2 commits May 19, 2026 23:43
The skip dated to PR #20195 when bg-commit had a documented "FCU N+1
reads stale state from DB" race. The actual race is in the tester
helper, not the FCU: insertPoSBlocks waits on pre-commit state-change
events from the gRPC stream, then InsertChain opens an roTx and reads
ReadHeader/HeadBlockHash before the bg goroutine has committed.

Fix in InsertChain itself by calling ExecModule.WaitIdle (acquires
then releases the FCU semaphore — the bg goroutine releases it only
after commit). With foreground commit the semaphore is already free,
so WaitIdle is an instant no-op.

FCU sequencing was never the issue: forkchoice.go:158's TryAcquire
+ retryBusy-on-Busy serialises consecutive FCUs against the prior
bg goroutine. The test passes under -race.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…BlockHash

When erigon runs in import mode under FcuBackgroundCommit=true, the
post-UpdateForkChoice write at the end of import_cmd.go::InsertChain
races the bg-commit goroutine: it can land WriteHeadBlockHash(lvh) in
MDBX before the bg goroutine flushes the overlay containing Headers /
BlockHash entries for the same block. Result on next startup:
ReadHeadBlockHash returns the hash, HeaderNumber(hash) returns nil,
BlockReader.CurrentBlock dereferences the nil and panics
(freezeblocks/block_reader.go:1409). This made hive ethereum/rpc-compat
fail at runner startup of the second erigon process.

The existing wait on the state-change stream only ensures the dispatcher
fired (events are dispatched pre-commit from the overlay), not that
MDBX has flushed. Add ExecutionModule().WaitIdle() before the Update —
WaitIdle acquires/releases the FCU semaphore, blocking until the bg
goroutine completes its flush+commit. No-op when bg-commit is off.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@yperbasis

Copy link
Copy Markdown
Member Author

Manually dispatched the three CI workflows that were skipped because the PR is in draft state:

Why I want them: this PR flips FcuBackgroundCommit to true and re-routes head-sensitive RPC reads through the SD overlay. The earlier auto hive run on commit d7bbe14 (run 26126332101) hit a real bug in the import path (nil-deref in BlockReader.CurrentBlock because the post-FCU WriteHeadBlockHash raced the bg-commit goroutine), fixed in commit 9e61f87. Want the dispatched run to confirm that fix, and to surface any other paths with the same shape across the broader hive + gloas surface.

yperbasis and others added 2 commits May 20, 2026 09:28
Copilot review caught a class of bugs in the FcuBackgroundCommit
migrations where the head lookup was overlay-wrapped but the dependent
read on the same tx was not. During the ~50ms bg-commit window the
head returns N+1 (from the SD overlay) while MDBX is still at N, so
the dependent read references a head the rest of its view can't see.

- bor_api_impl.go (GetAuthor): pass the same overlayTx to
  _blockReader.HeaderByNumber. Previously latestBlockNum was N+1 but
  HeaderByNumber read MDBX for N+1's canonical hash → nil → errUnknownBlock.

- eth_block.go (GetBlockTransactionCountByNumber): pass overlayTx to
  _blockReader.Body. Without this, blockNum can be N+1 while Body
  returns nil from committed MDBX, dropping the RPC to (nil, nil).

- parity_api.go (parity_listStorageKeys): revert to plain tx. There is
  no good overlay-aware version: the block overlay exposes table
  writes (TxNums included) but not the SD domain mem batch, so a
  RangeAsOf over kv.StorageDomain would still bypass the pending
  storage writes for an overlay-derived block number, yielding an
  inconsistent view.

- trace_filtering.go: revert the toBlock-via-overlay change. filterV3
  scans receipts/logs against dbtx; routing only the toBlock through
  the overlay leaves the upper bound past what the scan can see.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Sweep of the remaining migrated callsites for the same head-vs-data
inconsistency Copilot flagged in the first review pass: the head
lookup was routed through the SD overlay but the downstream reads
(log scans, witness/proof builds, setHead, blockWithSenders, binary
search) still operate on the plain committed tx, so during the
bg-commit window the function references a head its own follow-up
reads cannot see.

Reverted to plain tx (consistent committed view) in:

- eth_call.go (GetProof / CreateAccessList witness paths)
- overlay_api.go and erigon_receipts.go and eth_receipts.go
  (log range upper bounds — getLogsV3 scans against plain tx;
  eth_getLogs additionally guards on GetLatestExecutedBlockNumber(tx)
  which would otherwise fire "node is still syncing" for an
  overlay-derived upper bound)
- eth_simulation.go (guard followed by blockWithSenders on plain tx)
- debug_api.go (debug_setHead guard; SetHead acts on committed DB)
- debug_execution_witness.go (latestBlock gates a branch that reads
  txnums + seeks commitment on plain tx)
- erigon_block.go (GetBlockByTimestamp binary search uses
  HeaderByNumber on plain tx)

Kept overlay-aware where the function uses the result in-memory and
does no dependent DB read, or where the dependent read was extended
in the previous commit (bor_api_impl.go GetAuthor,
eth_block.go GetBlockTransactionCountByNumber): eth_system.go
BlockNumber/GasPrice/BaseFee/BlobBaseFee (in-memory) and the
GasPriceOracleBackend tx (wrapped once at construction so all
b.tx reads are overlay-aware), bor_api_impl.go header-only paths,
txpool_api.go Content/ContentFrom, eth_txs.go pool fallback,
graphql_api.go GetLatestBlockNumber, trace_filtering.go was already
reverted in the previous commit.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@yperbasis

Copy link
Copy Markdown
Member Author

Re-dispatched the three skipped workflows against HEAD 79ce7d94ef (includes the import_cmd WaitIdle fix from 9e61f87, the bor/eth_block overlay-consistency extensions from d0a8e31, and the head-vs-data revert sweep from 79ce7d9):

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 20 out of 20 changed files in this pull request and generated 1 comment.

Comment thread rpc/jsonrpc/parity_api.go
Pre-existing latent panic: rawdb.ReadCurrentBlockNumber returns *uint64
which is nil when HeadHeaderHash isn't set (fresh DB / pre-head state),
and the *bn dereference on the next line would crash the RPC handler.
Surface a user-facing error instead. Flagged by Copilot review of the
nearby diff in 79ce7d9.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@yperbasis
yperbasis marked this pull request as ready for review May 20, 2026 12:53
@yperbasis
yperbasis requested a review from mh0lt as a code owner May 20, 2026 12:53
@yperbasis
yperbasis requested a review from taratorio May 20, 2026 12:53
@yperbasis yperbasis added the RPC label May 20, 2026
@yperbasis
yperbasis marked this pull request as draft May 20, 2026 13:22
yperbasis and others added 4 commits May 20, 2026 15:53
eth_getLogs, trace_filter.Filter, and overlay_api.getBeginEnd previously
resolved the implicit `latest` reference through nil filters (plain tx,
returns N-1 during the bg-commit window) but routed user-passed
ToBlock/FromBlock="latest" through api.filters (overlay-aware, returns
N). The resulting `end > latest` guard would false-positive
errBlockRangeIntoFuture for ~50ms after every FCU.

Pass nil filters consistently in the explicit-tag resolution so the
upper bound, the lower bound, and the underlying getLogsV3/filterV3 scan
all agree on the committed view.

Pre-existing in foreground-commit mode too, but only observable during
the brief commit window when the CL hadn't yet received the FCU
response; becomes user-facing with FcuBackgroundCommit=true defaulting
on.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…ByTimestamp and debug_setHead

Two callsites previously reverted to plain tx in 79ce7d9 only touch
block tables (no SD-temporal reads), so overlay-aware reads are fully
consistent with their dependent reads.

- erigon_block.GetBlockByTimestamp: wrap tx once at the top; route
  ReadCurrentHeader, the inner _blockReader.HeaderByNumber binary-search
  probe, and the three buildBlockResponse callsites through overlayTx.

- debug_setHead: read currentHead through the overlay so
  debug_setHead(N) during the bg-commit window doesn't false-positive
  "block N is in the future" for what is effectively a no-op rollback.

The SD-temporal reverts (eth_call/getProof, log scans, simulation,
commitment seek, parity_listStorageKeys) still need the SD-aware
temporal view tracked in #21314.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…mmit semantics and known limitation

Document the trade-offs of routing head reads through the SD block
overlay during the bg-commit window:

- node/ethconfig/config.go: expand the FcuBackgroundCommit doc block to
  cover the FCU semaphore sequencing, embedded vs. remote rpcdaemon
  semantics, and the eth_call(latest) header-vs-state lag (~50ms). Link
  to #21314 for the proper SD-aware view follow-up.

- rpc/jsonrpc/eth_call.go: note that BlockOverlayTemporalTx wraps table
  reads but delegates temporal methods to roTx; link to #21314.

- rpc/jsonrpc/eth_simulation.go: comment was wrong — blockWithSenders
  auto-wraps. The real reason latest stays on plain tx is
  NewSharedDomains ties the simulator to the plain tx's domain state.

- rpc/jsonrpc/eth_system.go: explain why GasPriceOracleBackend.Fork
  opens a non-overlay-wrapped tx (downstream helpers re-wrap
  internally).

- execution/execmodule/exec_module_test.go: replace stale "subsequent
  blocks may fail validation" comment on TestNotificationDispatchBackgroundCommit
  — the semaphore serializes FCUs so N+1 always reads N's committed
  state.

- cmd/utils/flags.go + docs (configuring-erigon.mdx, llms-full.txt):
  update --fcu.background.commit usage and default; flag a concise
  rpcdaemon caveat in the description.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…ay safe-close invariant and assert memStore backing

RPC readers acquire views via Filters.WithOverlay / WithTemporalOverlay /
SharedDomains.BlockOverlayTemporalTx that share memTx with the published
SD's BlockOverlay. The FCU bg-commit goroutine closes that SD while
those readers are still iterating, and the only thing keeping the
pattern safe today is that the BlockOverlay is backed by
membatchwithdb.NewMemoryBatch — a pure-Go memStore whose
Rollback/Close are no-ops on the in-memory data (memory_store.go), and
that views hold their own caller-supplied backing tx. Both properties
were load-bearing but undocumented; flagged in the FcuBackgroundCommit=true
review as a latent risk.

Make the invariant explicit:

- MemoryMutation.Rollback (memory_mutation.go): comment explaining the
  no-op semantics for memStore-backed batches and the consequence for
  concurrent read views.

- MemoryMutation.NewReadView (memory_mutation.go): concurrency note
  pointing to newReadViewMut.

- MemoryMutation.newReadViewMut (memory_mutation.go): runtime panic if
  m.memTx is not *memStore. Catches the case where someone swaps
  NewMemoryBatch for NewMemoryBatchMDBX (which DOES invalidate cursors
  on Rollback) and forgets that the safe-concurrent-close property
  vanishes. The panic message points at the required follow-up
  (refcount/drain) for any MDBX-backed overlay with concurrent readers.

- SharedDomains.Close (domain_shared.go): comment walking through each
  of sd.mem / sd.blockOverlay / sd.sdCtx and why concurrent RPC views
  remain safe under each close path.

- Filters.WithOverlay / WithTemporalOverlay (filters.go): public-API
  documentation of the safe-concurrent-close property and the standard
  caller-tx-lifecycle constraint that still applies.

No behaviour change for the supported (memStore-backed) path; the panic
fires only if an unsupported backing store is wired in.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@yperbasis
yperbasis marked this pull request as ready for review May 20, 2026 14:19
@yperbasis
yperbasis requested a review from bloxster as a code owner May 20, 2026 14:19
NewMemoryBatch is the only constructor left, so the read-view safety
invariant newReadViewMut asserted at runtime is now compile-time: type
the field *memStore and drop the panic.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 36 out of 36 changed files in this pull request and generated 2 comments.

Comment thread rpc/jsonrpc/eth_call.go
Comment thread rpc/jsonrpc/eth_call.go

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 37 out of 37 changed files in this pull request and generated 5 comments.

Comment thread rpc/jsonrpc/eth_call.go
Comment thread rpc/rpchelper/helper.go Outdated
Comment thread llms-full.txt Outdated
Comment thread docs/site/static/llms-full.txt Outdated
Comment thread docs/site/docs/fundamentals/modules/rpc-daemon.md Outdated
rpc-daemon.md still described the removed NewSimple fallback ("fall back
to a non-versioned last-block cache"); use the config.go wording and
regenerate llms-full.txt via generate-llms.py.
At a zero budget the eviction loop drains every insert straight back
out, so Get/GetCode took the global lock twice and churned the btree
and eviction list per read-through, and OnNewBlock fed-then-evicted
every batch entry. Skip the lookup and the add when the budget is zero;
reads fall through to the caller's tx snapshot as before. Zero budget
is the standalone rpcdaemon default until #22269 raises it.

Behavior-preserving: TestZeroBudgetRetainsNothing pins the contract
(nothing retained, reads resolve on the tx snapshot rather than
announced batch data) and was confirmed green before the change too.
resolveLogsRange only needs the block number, but BlockByHash decodes
the full body and recovers senders on every eth_getLogs/overlay_getLogs
call with a blockHash filter. A header without a body (sync window,
pruned bodies) now resolves and hits the clearer downstream guards
(latest-executed check, checkReceiptsAvailable) instead of a generic
"block not found".
Resolves a semantic conflict with #22467, which deleted
SharedDomains.DetachBranchCache on main: drop the getProof call site.
Isolation from concurrent commits now comes from the shared branch
cache's bound gating (servableUnderBound) instead of detaching;
TestGetProofPinsReadSnapshot pins that the proof still resolves on the
caller's RO snapshot.
Tags resolve against the view tx exposes — nil filters does not force
the committed view; wrap-once callers pass an overlay tx with nil
filters. filters only controls the internal overlay wrap and whether
"pending" may resolve via LastPendingBlock.
@yperbasis

Copy link
Copy Markdown
Member Author

Splitting this PR into independently-mergeable pieces, all based on current main (none stacked), kept as drafts:

The union of the four reproduces this PR's diff exactly; only rpc/rpchelper/helper.go and the generated llms files are touched by two PRs, with non-overlapping hunks. #22269 (the default flip) conceptually follows #22532 + #22535.

yperbasis added a commit that referenced this pull request Jul 17, 2026
Moved from #21293: with the FCU response returning pre-commit, the
version-keyed Coherent cache is what keeps a standalone daemon's reads
consistent with its committed view instead of serving pre-commit
state-change data against an older head. --state.cache=0 remains the
no-retention escape hatch (snapshot-consistent, nothing cached).
@yperbasis

Copy link
Copy Markdown
Member Author

Closing in favor of the split (map in the comment above): #22532 (coherent version-keyed state cache), #22533 (RPC overlay/committed view split), #22534 (membatchwithdb safe-close invariant), #22535 (bg-commit consumers + flag docs) — with #22269 (default flip) and #22278 (SimpleCache rename) re-stacked onto #22532. Together they reproduce this branch's diff exactly.

The "Interplay with #21414" checklist from this description now lives as a comment on #21414, mapped to the split PRs. The branch stays in place for reference.

@yperbasis yperbasis closed this Jul 17, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants